﻿// HSS.Forms.StringParser.cs
// ----------------------------------------------------------------------------
// Licensed under the MIT license
// http://www.opensource.org/licenses/mit-license.html
// ----------------------------------------------------------------------------
// HighSpeed-Solutions, LLC
// Copyright (c) 2001-2010
// ----------------------------------------------------------------------------
// File:       StringParser.cs
// Author:     HSS\gbanta
// Created:    08/12/2010
// Modified:   12/04/2010
// ----------------------------------------------------------------------------
namespace HSS.Forms
{
	#region Using Directives
	using System;
	using System.Collections.Generic;
	using System.Reflection;
	using System.Text;
	#endregion

	#region StringParser
	/// <summary>
	/// This class parses strings wrapped in the following pattern ${} with the value being
	/// stored in either the <c>Properties</c> or <c>PropertyObjects</c> properties.
	/// <para>
	/// If <c>Properties</c> has a key/pair value of 
	/// key='name', value='Joe', and then call Parse("${name}"), it would return 'Joe'. You could also
	/// use the <c>PropertyObjects</c> to store Objects containing properties or fields
	/// and parse them. For example if you stored an object objA with a property or field named
	/// UserName with a value of 'Tom', and stored objA with a key of 'logon', then you could retrieve the value
	/// by calling Parse("${logon:UserName}") which would return 'Tom'.
	/// </para>
	/// <para>
	/// Environment variables are also avaible using <c>env:[NAME]</c>
	/// where [NAME] represents the string under which it is avaiable in
	/// the environment. Example: Parse("${env:TEMP}")
	/// </para>
	/// <para>
	/// String Resources are also avaible using <c>res:[NAME]</c>
	/// where [NAME] represents the string under which it is avaiable in
	/// the <seealso cref="ResourceService"/>. Example: Parse("${res:DialogTitle}")
	/// </para>
	/// <para>
	/// It will also return Date, Time and Guid, by calling Parse("Date")
	/// or Parse("Time") or Parse("Guid").
	/// </para>
	/// <para>
	/// Also you can register a <seealso cref="IStringTagProvider"/> that contains a list 
	/// of string tags. Then calling parse passing in the tag desired, will return that string.
	/// You can also inline the process by passing a list of custom tags.
	/// Parse("${customname}", [,] customtags) would return the value from the array of custom tags
	/// assuming the customname existed in the array.
	/// </para>
	/// <para>
	/// Finally, using the data available in <seealso cref="AppLocalUserData"/> you
	/// can parse out those values by calling Parse("${appdata:[NAME}}")
	/// </para>
	/// </summary>
	public static class StringParser
	{
		readonly static Dictionary<string, IStringTagProvider> stringTagProviders;

		static StringParser()
		{
			properties = new Dictionary<string, string>(StringComparer.InvariantCultureIgnoreCase);
			stringTagProviders = new Dictionary<string, IStringTagProvider>(StringComparer.InvariantCultureIgnoreCase);
			propertyObjects = new Dictionary<string, object>();
		}

		/// <summary>
		/// List of properties available to the Parser
		/// </summary>
		public static Dictionary<string, string> Properties
		{
			get { return properties; }
		} readonly static Dictionary<string, string> properties;

		/// <summary>
		/// List of objects associated with with properties available to the Parser
		/// </summary>
		public static Dictionary<string, object> PropertyObjects
		{
			get { return propertyObjects; }
		} readonly static Dictionary<string, object> propertyObjects;

		/// <summary>
		/// Parse an input string returned the desired string value
		/// </summary>
		/// <param name="input">The string to parse</param>
		/// <returns>The resolved string</returns>
		public static string Parse(string input)
		{
			return Parse(input, null);
		}

		/// <summary>
		/// Parses an array and replaces the elements in the existing array.
		/// </summary>
		public static void Parse(string[] inputs)
		{
			for (int i = 0; i < inputs.Length; ++i)
				inputs[i] = Parse(inputs[i], null);
		}

		/// <summary>
		/// Register a TagProvider
		/// </summary>
		/// <param name="tagProvider">The provider to register</param>
		public static void RegisterStringTagProvider(IStringTagProvider tagProvider)
		{
			foreach (string str in tagProvider.Tags)
				stringTagProviders[str] = tagProvider;
		}

		/// <summary>
		/// Expands ${xyz} style property values.
		/// </summary>
		public static string Parse(string input, string[,] customTags)
		{
			if (input == null)
				return null;

			int pos = 0;
			StringBuilder output = null; // don't use StringBuilder if input is a single property
			do
			{
				int oldPos = pos;
				pos = input.IndexOf("${", pos);

				if (pos < 0)
				{
					if (output == null)
						return input;
					else
					{
						if (oldPos < input.Length)
							output.Append(input, oldPos, input.Length - oldPos); // normal text after last property
						return output.ToString();
					}
				}

				if (output == null)
				{
					if (pos == 0)
						output = new StringBuilder();
					else
						output = new StringBuilder(input, 0, pos, pos + 16);
				}
				else
				{
					if (pos > oldPos)
						output.Append(input, oldPos, pos - oldPos); // normal text between two properties
				}

				int end = input.IndexOf('}', pos + 1);
				if (end < 0)
				{
					output.Append("${");
					pos += 2;
				}
				else
				{
					string name = input.Substring(pos + 2, end - pos - 2);
					string val = GetValue(name, customTags);
					if (val == null)
					{
						output.Append("${");
						output.Append(name);
						output.Append('}');
					}
					else
						output.Append(val);

					pos = end + 1;

				}
			} while (pos < input.Length);
			return output.ToString();
		}

		static string GetValue(string propertyName, string[,] customTags)
		{
			if (propertyName.StartsWith("res:"))
			{
				// most properties start with res: in lowercase,
				// so we can safe 2 string allocations here
				try { return Parse(ResourceService.GetString(propertyName.Substring(4)), customTags); }
				catch (ResourceNotFoundException) { return null; }
			}
			if (propertyName.Equals("DATE", StringComparison.OrdinalIgnoreCase))
				return DateTime.Today.ToShortDateString();
			if (propertyName.Equals("TIME", StringComparison.OrdinalIgnoreCase))
				return DateTime.Now.ToShortTimeString();
			if (propertyName.Equals("GUID", StringComparison.OrdinalIgnoreCase))
				return Guid.NewGuid().ToString().ToUpperInvariant();

			if (customTags != null)
			{
				for (int j = 0; j < customTags.GetLength(0); ++j)
				{
					if (propertyName.Equals(customTags[j, 0], StringComparison.OrdinalIgnoreCase))
						return customTags[j, 1];
				}
			}

			if (properties.ContainsKey(propertyName))
				return properties[propertyName];

			if (stringTagProviders.ContainsKey(propertyName))
				return stringTagProviders[propertyName].Convert(propertyName);

			int k = propertyName.IndexOf(':');
			if (k <= 0)
				return null;

			string prefix = propertyName.Substring(0, k);
			propertyName = propertyName.Substring(k + 1);
			switch (prefix.ToUpperInvariant())
			{
				case "ENV":
					return Environment.GetEnvironmentVariable(propertyName);
				case "RES":
					try { return Parse(ResourceService.GetString(propertyName), customTags); }
					catch (ResourceNotFoundException) { return null; }
				case "APPDATA":
					return GetAppData(propertyName);
				default:
					if (propertyObjects.ContainsKey(prefix))
						return Get(propertyObjects[prefix], propertyName);
					else
						return null;
			}
		}
		static string GetAppData(string key)
		{
			string defaultValue = "";
			int pos = key.LastIndexOf("??");
			if (pos >= 0)
			{
				defaultValue = key.Substring(pos + 2);
				key = key.Substring(0, pos);
			}
			pos = key.IndexOf('/');
			if (pos >= 0)
			{
				AppData data = AppLocalUserData.Get(key.Substring(0, pos), new AppData());
				key = key.Substring(pos + 1);
				pos = key.IndexOf('/');
				while (pos >= 0)
				{
					data = data.Get(key.Substring(0, pos), new AppData());
					key = key.Substring(pos + 1);
				}
				return data.Get(key, defaultValue);
			}
			else
			{
				return AppLocalUserData.Get(key, defaultValue);
			}
		}
		static string Get(object obj, string name)
		{
			Type type = obj.GetType();

			PropertyInfo prop = type.GetProperty(name);
			if (prop != null)
				return prop.GetValue(obj, null).ToString();

			FieldInfo field = type.GetField(name);
			if (field != null)
				return field.GetValue(obj).ToString();

			return null;
		}
	}
	#endregion
}